import md5

try:
    import cPickle as Pickle
except ImportError:
    import Pickle


class DeliveryError(Exception):
    """An Exception subclass for message and response delivery failures."""
    pass


class Message(object):

    def __init__(self, sender_id, rcpt_id, msg, args=[], kwargs={}):
        # For now, just store information. No controller registration is
        # carried out yet, as this allows us to build a message instance and
        # tweak it before it is finally sent out.
        self.id = None
        self.sender = sender_id
        self.recipient = rcpt_id
        self.message = msg
        self.arguments = args
        self.keyword_arguments = kwargs

    def digest(self):
        # A horribly quick-and-dirty way of getting a message digest, to ensure
        # that transport (especially across networks) is reliable.
        return md5.md5(Pickle.dumps(self)).hexdigest()

    def __dict__(self):
        dictout = {}
        dictout['id'] = self.id or 'None'
        for attr in [ 'sender', 'recipient', 'message', 'arguments',
            'keyword_arguments' ]:
            dictout[attr] = getattr(self, attr)
        dictout['md5'] = self.digest()
        return dictout

    @classmethod
    def from_dict(cls, dictin):
        # Creating a ``classmethod`` allows for instantiation of the class
        # without requiring an instance to already exist. A very simple
        # unpacking of the dictionary's keys and values into a ``Message``
        # instance.
        msg_out =  cls(
            dictin['sender'],
            dictin['recipient'],
            dictin['message'],
            args=dictin['arguments'],
            kwargs=dictin['keyword_arguments'])
        if dictin['id'] != 'None':
            msg_out.id = dictin['id']
        # Validate message by comparing stored md5 digest with newly calculated
        # digest. This is useful in determining if the message has been
        # corrupted in transit (esp. when actors span a network).
        assert msg_out.digest() == dictin['md5'], \
            'Corrupt message (invalid md5sum)'
        return msg_out

    def to_pickle(self):
        # Dump the pickle of the dictionary instead, as it reduces issues with
        # pickling non-stdlib classes.
        return Pickle.dumps(dict(self))

    @classmethod
    def from_pickle(cls, pickle_string):
        # Just load the pickled dictionary and unpack it into a ``Message``
        # instance using ``Message.from_dict``.
        dictin = Pickle.loads(pickle_string)
        return cls.from_dict(dictin)

class Response(object):

    # Most of this class definition is very similar to that for ``Message``, so
    # see the comments on the ``Message`` class for more info.

    def __init__(self, msg, return_value=None, exception=None):
        self.id = None
        self.sender = msg.recipient
        self.recipient = msg.sender
        self.value = return_value
        self.orig_msg = msg
        self.exception = exception

    def digest(self):
        return md5.new(Pickle.dumps(self)).hexdigest()

    def __dict__(self):
        # This definition is slightly different to that for
        # ``Message.__dict__``, mainly due to the fact that this has to hold a
        # ``Message`` object containing the original message.
        dictout = {}
        dictout['id'] = self.id or 'None'
        dictout['sender'] = self.sender
        dictout['recipient'] = self.recipient
        dictout['value'] = Pickle.dumps(self.value)
        dictout['exception'] = Pickle.dumps(self.exception)
        dictout['orig_msg'] = dict(self.orig_msg)
        dictout['md5'] = self.digest()
        return dictout

    @classmethod
    def from_dict(cls, dictin):
        # Very similar to ``Message.from_dict``, only some attributes have to
        # be unpickled themselves.
        msg = Message.from_dict(dictin['orig_msg'])
        value = Pickle.loads(dictin['value'])
        exception = Pickle.loads(dictin['exception'])
        response = cls(msg, value)
        response.exception = exception
        if dictin['id'] != 'None':
            response.id = dictin['id']
        assert response.digest() == dictin['md5'], \
            'Corrupt response (invalid md5sum)'
        return response

    def to_pickle(self):
        return Pickle.dumps(dict(self))

    @classmethod
    def from_pickle(cls, pickle_string):
        dictin = Pickle.loads(pickle_string)
        return cls.from_dict(dictin)